Appearance
[中文版本](/posts/c教程/zh-CN/07-贴近系统/064-Pipes and Redirection)
Now that you can read and write with file descriptors, you can connect two processes so that one’s output becomes the other’s input, just likels | grep c in a shell.
This magic happens through pipes, one of Unix’s simplest and most elegant inter-process communication (IPC) mechanisms.
Step 1. What Is a Pipe?
A pipe is a unidirectional data channel between two file descriptors, one for reading, one for writing.
In the shell:
ls | grep mainis equivalent to:
- Process A (
ls) writes into the pipe. - Process B (
grep) reads from the pipe.
In C, you can do the same thing usingpipe() andfork().
Step 2. Creating a Pipe
#include <unistd.h>
#include <stdio.h>
int main(void) {
int fds[2];
if (pipe(fds) == -1) {
perror("pipe failed");
return 1;
}
printf("Read end: %d, Write end: %d\n", fds[0], fds[1]);
return 0;
}Compile and run:
gcc pipe_demo.c -o pipe_demo
./pipe_demoOutput example:
Read end: 3, Write end: 4You now have two connected file descriptors:
fds[0]: read endfds[1]: write end
Whatever you write intofds[1] can be read fromfds[0].
Step 3. Writing and Reading Through a Pipe
#include <unistd.h>
#include <string.h>
#include <stdio.h>
int main(void) {
int fds[2];
pipe(fds);
const char msg[] = "hello through pipe";
write(fds[1], msg, strlen(msg));
char buf[64];
ssize_t n = read(fds[0], buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("Received: %s\n", buf);
close(fds[0]);
close(fds[1]);
}Output:
Received: hello through pipeYou’ve just communicated through memory between two file descriptors, no files, no network.
Step 4. Pipes Between Parent and Child
Here’s where it gets powerful: pipes can connect processes.
#include <unistd.h>
#include <stdio.h>
#include <string.h>
int main(void) {
int fds[2];
pipe(fds);
pid_t pid = fork();
if (pid == 0) {
// Child
close(fds[1]); // close write end
char buf[64];
ssize_t n = read(fds[0], buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("Child got: %s\n", buf);
} else {
// Parent
close(fds[0]); // close read end
const char msg[] = "Hi from parent!";
write(fds[1], msg, strlen(msg));
close(fds[1]);
}
}Output:
Child got: Hi from parent!Parent writes, child reads, a clean data channel between two processes.
Step 5. Redirecting STDIN/STDOUT to a Pipe
You can usedup2() to connect a pipe directly to standard input/output.
Example: connect parent’s write end to child’s stdin.
#include <unistd.h>
#include <stdio.h>
int main(void) {
int fds[2];
pipe(fds);
pid_t pid = fork();
if (pid == 0) {
dup2(fds[0], STDIN_FILENO);
close(fds[1]);
execlp("wc", "wc", "-w", NULL);
} else {
close(fds[0]);
write(fds[1], "Hello from parent\nThis is a pipe test\n", 39);
close(fds[1]);
}
}Output:
6Here’s what happens:
- Parent writes data into the pipe.
- Child’s stdin is connected to that pipe.
wc -wcounts the words received.
This is exactly how the shell implements pipelines likeecho "hi" | wc -w.
Step 6. Chaining Multiple Commands
You can chain multiple commands by creating multiple pipes and connecting them in series.
Example concept:
cat file.txt | grep "error" | wc -lEach process reads from the previous pipe and writes to the next, the shell’s fundamental design.
You can implement the same concept in C by:
- Creating a pipe between each process pair.
- Forking a new process for each command.
- Redirecting its stdin/stdout via
dup2().
Step 7. Named Pipes (FIFOs)
Pipes normally exist only between related processes. To share data between unrelated programs, you can use named pipes (FIFOs).
Create one:
mkfifo mypipeThen in one terminal:
cat > mypipeAnd in another:
cat < mypipeData flows through the named pipe like a file.
In C:
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
int main(void) {
mkfifo("mypipe", 0666);
int fd = open("mypipe", O_WRONLY);
write(fd, "Hello FIFO\n", 11);
close(fd);
}Step 8. Error Handling and EOF
If all write ends of a pipe are closed,read() returns0, indicating EOF.
int fds[2];
pipe(fds);
close(fds[1]); // no writers
char buf[10];
ssize_t n = read(fds[0], buf, 10); // n == 0 => EOFIf you try to write after all readers are gone, you’ll getSIGPIPE.
Tiny Code: Minimal Shell Pipeline
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int fds[2];
pipe(fds);
if (fork() == 0) {
dup2(fds[1], STDOUT_FILENO);
close(fds[0]);
execlp("ls", "ls", NULL);
}
if (fork() == 0) {
dup2(fds[0], STDIN_FILENO);
close(fds[1]);
execlp("wc", "wc", "-l", NULL);
}
close(fds[0]);
close(fds[1]);
wait(NULL);
wait(NULL);
}Run:
gcc pipe_chain.c -o pipe_chain
./pipe_chainOutput:
(number of files in directory)You just recreatedls | wc -l in C.
Step 9. Combining Redirection and Files
You can redirect stdout or stderr to files the same way:
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
close(fd);
execlp("ls", "ls", NULL);Now the output ofls goes tooutput.txt.
Step 10. Why It Matters
Pipes and redirection are the heart of Unix philosophy:
- Programs do one thing well.
- Communicate through text streams.
- Compose complex workflows by chaining simple tools.
Understanding how to implement pipes makes you capable of:
- Building your own shell
- Connecting processes dynamically
- Implementing inter-process communication safely
Try It Yourself
- Recreate
ls | grep cusing twofork()calls and one pipe. - Build
cat file | wc -l. - Implement a “tee” program that duplicates output to both stdout and a file.
- Create a named pipe and write to it from one process, read from another.
- Extend your shell to support pipelines of any length.
Next, you’ll explore how processes signal and interrupt each other, using signals and signal handlers, a crucial concept for handling interrupts, timeouts, and graceful termination.
